from PIL import Image,ImageFont,ImageDraw
from typing import List, Tuple, Any, Dict

def getOppositeColour(color:Tuple[int,int,int]) -> Tuple[int,int,int]:
    """
    Returns the opposite color by subtracting each RGB value from 255.
    
    Args:
        color (Tuple[int, int, int]): A tuple representing the RGB color values.
    
    Returns:
        Tuple[int, int, int]: A tuple representing the opposite RGB color values.
    """
    return (255-color[0],255-color[1],255-color[2])

class ColorMap():
    def __init__(self, image: Image.Image, size :Tuple[int,int]=(80,80), num: int = 10 , font:ImageFont.FreeTypeFont=ImageFont.truetype("arial.ttf", 20),
                 background=(255,255,255),color=(0,0,0)):
        self.image = image
        self.num = num
        self.size = size
        self.font = font
        self.background=background
        self.color=color
        self.paletteToColorMap: Dict[int, int] = {}
        self._pModeImage: Image.Image | None = None
        self._pretreatedImage: Image.Image | None = None
        self._palette: List[Tuple[int, int, int]] | None = None
        self._colorConts: List[Tuple[int, int]] | None = None
        self._colorMap: List[Tuple[int,int,int]]|None = None
        self._imageMap: List[List[int]]|None = None
        self._printImage: Image.Image | None = None
        self._printColorMapImage: Image.Image | None = None
    @property
    def pretreatedImage(self) -> Image.Image:
        """
        Returns the pretreated image.
        
        :return: The pretreated image.
        :rtype: Image.Image
        """
        if self._pretreatedImage == None:
            self._pretreatedImage=self.image.resize(self.size,Image.ADAPTIVE)
        return self._pretreatedImage
    @property
    def pModeImage(self) -> Image.Image:
        """
        Return the property `pModeImage`.

        :return: The property `pModeImage` which is an instance of `Image.Image`.
        """
        if self._pModeImage == None:
            self._pModeImage = self.pretreatedImage.convert(
                "P", palette=Image.ADAPTIVE, colors=self.num
            )
        return self._pModeImage

    @property
    def palette(self) -> List[Tuple[int, int, int]]:
        """
        Returns the palette of the image.

        This property returns the palette of the image as a list of tuples, where each tuple represents an RGB color. If the palette has not been initialized, it retrieves the palette from the `pModeImage` attribute and stores it in the `_palette` attribute. The `_palette` attribute is then returned. If the image has no palette, a `ValueError` is raised.

        Returns:
            List[Tuple[int, int, int]]: The palette of the image as a list of RGB color tuples.
        """
        if self._palette==None:
            rawPalette = self.pModeImage.getpalette()
            if rawPalette == None:
                raise ValueError("The image has no palette.")
            self._palette=[]
            i=0
            while i+2 < len(rawPalette):
                self._palette.append((rawPalette[i], rawPalette[i+1], rawPalette[i+2]))
                i+=3
        return self._palette

    @property
    def colorConts(self) -> List[Tuple[int, int]]:
        """
        Get the color counts of the image.

        Returns:
            List[Tuple[int, int]]: A list of tuples containing the color counts of the image.
        """
        if self._colorConts==None:
            self._colorConts=sorted(self.pModeImage.getcolors(self.num), reverse=True)
        return self._colorConts
    @property
    def colorMap(self):
        """
        Returns the color map of the object.
        """
        if self._colorMap==None:
            self._colorMap=[]
            self.paletteToColorMap={}
            for i in self.colorConts:
                self.paletteToColorMap[i[1]]=len(self._colorMap)
                self._colorMap.append(self.palette[i[1]])
        return self._colorMap
    @property
    def imageMap(self):
        """
        Get the image map of the current object.

        Returns:
            list: The image map of the current object.
        """
        if self._imageMap==None:
            pixs=self.pModeImage.load()
            self.colorMap
            self._imageMap=[]
            for j in range(self.pModeImage.size[0]):
                self._imageMap.append([])
                for i in range(self.pModeImage.size[1]):
                    self._imageMap[-1].append(self.paletteToColorMap[pixs[j,i]])
        return self._imageMap
    @property
    def printImage(self):
        """
        Returns the image representation of the object.
        This property generates an image of the object using the provided font, color map, and image map.
        If the image has not been generated yet, it calculates the size of the image based on the font size and creates a new image object.
        It then uses the ImageDraw module to draw text on the image based on the image map and font parameters.
        The generated image is stored in the _printImage attribute and returned.

        Returns:
            PIL.Image.Image: The image representation of the object.
        """
        if self._printImage==None:
            k=int(max(self.font.getsize(str(len(self.colorMap)-1)))*1.2)
            newSize=(self.pModeImage.size[0]*k,self.pModeImage.size[1]*k)
            self._printImage=Image.new("RGB",newSize,self.background)
            draw=ImageDraw.Draw(self._printImage)
            for i in range(len(self.imageMap)):
                for j in range(len(self.imageMap[i])):
                    draw.text(((i+0.1)*k,(j+0.1)*k),str(self.imageMap[i][j]),font=self.font,fill=self.color)
        return self._printImage
    @property
    def printColorMapImage(self):
        """
        Returns the color map image for printing.

        :return: The color map image.
        """
        if self._printColorMapImage==None:
            (singleWidth,singleHeight)=self.font.getsize_multiline(f"0\nRGB(255, 255, 255)")
            self._printColorMapImage=Image.new("RGB",(int(singleWidth*len(self.colorMap)*1.2),int(singleHeight*3)),self.background)
            draw=ImageDraw.Draw(self._printColorMapImage)
            for i in range(len(self.colorMap)):
                draw.rectangle(((int(i*singleWidth*1.2),0),(int((i+1)*singleWidth*1.2),int(singleHeight*3))),fill=self.colorMap[i],width=0)
                draw.multiline_text((int(i*singleWidth*1.2)+0.1*singleWidth,singleHeight),f"{i}\nRGB{self.colorMap[i]}",font=self.font,fill=getOppositeColour(self.colorMap[i]),align="center")
        return self._printColorMapImage